Skip to content

Conversation

@eliykat
Copy link
Member

@eliykat eliykat commented Jan 8, 2026

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-22236

📔 Objective

There is a known defect where user provisioning via the SSO AccountController incorrectly creates an OrganizationUser in the Invited status, but also links it to a UserId. This violates the expected contract for OrganizationUser objects.

This is generally not a problem because the user accepts the invite immediately and moves into a valid accepted state. However, it's not an atomic process, so if it goes off the happy path for any reason, the user will be left in this invalid invited state.

One side effect is that if the user's domain matches a domain claimed by the organization, they will be considered "claimed" in some contexts even though they are in the invited status. This is because we join through the UserId assuming that this will be null for invited OrganizationUsers; but the bug above invalidates this assumption. The result is that the user is stuck in a state that is simultaneously claimed and not claimed, and requires manual intervention by our CS team.

The solution here is to expressly exclude invited users from these queries. The queries are:

  • Organization_ReadByClaimedUserEmailDomain - generally used to block user-driven actions such as deleting their account or purging their vault
  • OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2 - used to display the list of members in the Admin Console with the correct actions

Note that this does not help users stuck in this invalid state and then revoked. Because revoked is its own status, this requires a workaround to detect the underlying status - probably checking for revoked + userId not null + email not null, similar to the OrganizationService method. However that is creating more workarounds to clean up later so I would rather leave this unhandled for now, noting that it does not pose any security risks. Instead we will work with Auth team to fix this edge case and stop these invalid states from arising (Jira ticket).

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@eliykat eliykat added the ai-review Request a Claude code review label Jan 8, 2026
@eliykat eliykat requested review from a team as code owners January 8, 2026 04:16
@eliykat eliykat requested a review from JimmyVo16 January 8, 2026 04:16
@claude
Copy link
Contributor

claude bot commented Jan 8, 2026

Claude finished @eliykat's task in 2m 42s —— View job


Code Review Summary

I've completed my review of this PR. The implementation correctly addresses the defect where invited users with UserId set are incorrectly considered "claimed" by organization domains. The fix is well-implemented, thoroughly tested, and ready to merge.

Summary

This PR excludes invited users (Status != 0) from claimed domain queries to fix a defect where users stuck in an invalid invited state (with UserId set) are incorrectly treated as claimed. The fix applies to both Organization_ReadByClaimedUserEmailDomain and OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2.

Verdict: APPROVED - No blocking issues found. All review feedback has been addressed.


What Was Reviewed

Changed Files:

  • 2 SQL stored procedures with defensive status checks
  • 2 EF Core implementations with matching logic
  • 2 migration scripts dated for future deployment (2026-01-14)
  • Comprehensive integration tests (moved to dedicated test files)
  • Updated interface documentation

Review Focus:

  • Security implications of excluding invited users
  • Consistency across SQL and EF Core implementations
  • Correctness of Status != 0 check (excludes Invited = 0, includes Accepted = 1, Confirmed = 2, Revoked = -1)
  • Test coverage for edge cases
  • Adherence to review feedback

Positive Findings

  1. Excellent test coverage - Edge cases thoroughly tested including the specific defect scenario (invited user with UserId)
  2. Consistent implementation - All three query paths (SQL, SQL V2, EF Core) apply the same exclusion logic
  3. Good refactoring - Tests moved to dedicated files for better organization
  4. Performance improvement - CTE in Organization_ReadByClaimedUserEmailDomain.sql optimized from SELECT U.* to SELECT U.[Id]
  5. Clear documentation - Interface comments updated to clarify that invited members are excluded
  6. Review feedback addressed - Comments added per mkincaid-bw's request

Technical Validation

Correctness of Status != 0 check:

  • Invited = 0 → Excluded ✓ (correct - these are not claimed users)
  • Accepted = 1 → Included ✓ (correct - accepted users should be claimed)
  • Confirmed = 2 → Included ✓ (correct - confirmed users should be claimed)
  • Revoked = -1 → Included ✓ (documented as intentional - revoked users remain claimed)

Security assessment:

  • Fix is defensive and conservative
  • Prevents incorrect claimed status that blocks user-driven actions (account deletion, vault purging)
  • Does not introduce new security risks
  • Maintains zero-knowledge principles

Notes

  1. Migration dates - Scripts dated 2026-01-14 (future date) appear intentional per commit history
  2. Revoked users - Intentionally not excluded from claimed status, as documented in test comments
  3. Known limitation - Does not fix users already in invalid revoked+invited state, but this edge case has not caused issues and will be addressed separately (PM-22405)

No inline comments needed - The implementation is clean and follows established patterns.

Generated by Claude Code at commit d9ca9df84

@codecov
Copy link

codecov bot commented Jan 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.64%. Comparing base (b9d1a35) to head (d9ca9df).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6810      +/-   ##
==========================================
+ Coverage   55.72%   59.64%   +3.91%     
==========================================
  Files        1949     1949              
  Lines       86539    86541       +2     
  Branches     7725     7725              
==========================================
+ Hits        48227    51615    +3388     
+ Misses      36506    33039    -3467     
- Partials     1806     1887      +81     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

Logo
Checkmarx One – Scan Summary & Details812c405b-4858-47d0-ad64-3b234b90168b

Fixed Issues (1)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs: 511

SET NOCOUNT ON;

WITH OrgUsers AS (
SELECT *
Copy link
Contributor

@JimmyVo16 JimmyVo16 Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an observation: It looks like this is existing code, so it’s not your change. That said, in general I think we should move away from * and only select what we actually need.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree! For now this returns a whole OrgUser object so we need to select the whole row, but we should move towards more targeted data objects.

I did find one unnecessary SELECT * that I removed though.

od.VerifiedDate != null &&
o.Enabled == true
o.Enabled == true &&
ou.Status != OrganizationUserStatusType.Invited
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud here lol:

I’ve noticed we have a few cases where we exclude certain collection types or user types. I agree that we should do filtering in the database to leverage the query optimizer and reduce sending unnecessary data over the network. However, I wonder if we could configure the sproc's parameters to include only the types we want returned. That way, we can control the types from the app in the future, reduce the need for an extra deployment, and avoid potential backward compatibility issues.

That said, this does add upfront complexity to the sproc for a situation that might never happen.

Copy link
Member Author

@eliykat eliykat Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do that in some sprocs, but here the status is closely tied to the purpose of the sproc: it's dealing with claimed accounts, and you can't claim an invited user's account. I would say this is a case where it should not be parameterized.

JimmyVo16
JimmyVo16 previously approved these changes Jan 9, 2026
WHERE OD.[VerifiedDate] IS NOT NULL
AND CU.EmailDomain = OD.[DomainName]
AND O.[Enabled] = 1
AND OU.[Status] != 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be handy to have the status documented in a comment like you did in the other proc
https://contributing.bitwarden.com/contributing/code-style/sql/#comments-and-documentation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review needs-qa

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants